/*
 * $Id: PlaylistService.java 376 2008-04-15 02:59:43Z jasta $
 *
 * Copyright (C) 2008 Josh Guilfoyle <jasta@devtcg.org>
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version 2, or (at your option) any
 * later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 */

package org.devtcg.five.music.service;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.Service;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Intent;
import android.content.ServiceConnection;
import android.database.Cursor;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.DeadObjectException;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteCallbackList;
import android.util.Log;
import android.widget.Toast;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.devtcg.five.music.R;
import org.devtcg.five.music.activity.Playlist;
import org.devtcg.five.provider.Five;
import org.devtcg.five.service.ContentState;
import org.devtcg.five.service.IContentService;
import org.devtcg.five.service.IContentObserver;

public class PlaylistService extends Service
{
	public static final String TAG = "PlaylistService";

	private MediaPlayer mPlayer = null;
	private boolean mIsPlaying = false;
	private boolean mIsPaused = false;
	private boolean mIsBuffering = false;

	/* Currently buffering content awaiting playback.  This concept needs a 
	 * lot of work... */
	private final Set<Long> mPending =
	  Collections.synchronizedSet(new HashSet<Long>());

	/* Active downloads. */
	private final Map<Long, MyContentObserver> mDownloads = 
	  Collections.synchronizedMap(new HashMap<Long, MyContentObserver>());

	private String mPlayingPathHack;
	private long mSongPlayingHack;

	private NotificationManager mNM;
	private boolean mNotificationUp = false;

	private IContentService mService;

	private final List<Long> mPlaylist = new LinkedList<Long>();
	private int mPos = -1;
	
	/* TOTAL HACK! */
	private NextSongGenerator mImpliedNext;

	private Handler mHandler = new Handler();

	private RemoteCallbackList<IPlaylistMoveListener> mMoveListeners =
	  new RemoteCallbackList<IPlaylistMoveListener>();

	private RemoteCallbackList<IPlaylistBufferListener> mBufferListeners =
	  new RemoteCallbackList<IPlaylistBufferListener>();

	@Override
	public void onCreate()
	{
		mNM = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);

		bindContentService();

		mImpliedNext = new NextSongGenerator(getContentResolver());

		/* TODO: Stop the service after so many minutes of inactivity
		 * (nothing playing). */
	}

	@Override
	public IBinder onBind(Intent intent)
	{
		return mBinder;
	}

	@Override
	public void onDestroy()
	{
		Log.d(TAG, "onDestroy");
		
		mImpliedNext.release();

		unbindContentService();

		if (mPlayer != null)
			mPlayer.release();

		if (mNotificationUp == true)
			mNM.cancel(R.string.app_name);

		super.onDestroy();
	}

	private void bindContentService()
	{
		ComponentName service = new ComponentName("org.devtcg.five",
		  "org.devtcg.five.service.ContentService");

		bindService(new Intent().setComponent(service),
		  mConnection, BIND_AUTO_CREATE);
	}

	private void unbindContentService()
	{
		unbindService(mConnection);
	}

	private ServiceConnection mConnection = new ServiceConnection()
	{
		public void onServiceConnected(ComponentName name, IBinder service)
		{
			mService = IContentService.Stub.asInterface(service);
		}

		public void onServiceDisconnected(ComponentName name)
		{
			mService = null;
		}		
	};

	private MediaPlayer.OnCompletionListener mSongFinished = new MediaPlayer.OnCompletionListener()
	{
		public void onCompletion(MediaPlayer mp)
		{
			Log.d(TAG, "Song finished!");

			long songId = mPlaylist.get(mPos);

			/* XXX: Hack; if the download is still in progress, restart
			 * at this position.  Necessary until MediaPlayer supports
			 * streaming while simultaneously caching. */
			if (mDownloads.containsKey(songId) == true)
			{
				try {
					restartPlaybackHack();
				} catch (Exception e) {
					Log.d(TAG, "Damn", e);
				}
			}
			else
			{
				advanceInternal();
			}
		}
	};

	private long getContentId(long songId)
	{
		Uri song = ContentUris.withAppendedId(Five.Music.Songs.CONTENT_URI, songId);

		Cursor c = getContentResolver().query(song,
		  new String[] { "content_id" }, null, null, null);

		if (c.count() == 0)
			return -1;

		c.first();
		long contentId = c.getLong(0);
		c.close();

		return contentId;
	}

	private void jumpInternal(int pos)
	{
		int len = mPlaylist.size();

		if (pos >= len || pos < 0)
		{
			long songId;

			if (pos == len && 
			    (songId = mImpliedNext.getNextSongWithSideEffects()) >= 0)
			{
				mPlaylist.add(songId);
				
				if (mPlayer != null)
					mPlayer.stop();

				mPos = pos;
				playInternal();
				broadcastJump(pos);
			}
			else
			{
				stopInternal();
			}
		}
		else
		{
			if (mPlayer != null)
				mPlayer.stop();

			mPos = pos;
			playInternal();
			broadcastJump(pos);
		}
	}

	private void advanceInternal()
	{
		int len = mPlaylist.size();

		Log.d(TAG, "len=" + len + ", mPos=" + mPos);

		if (mPos + 1 < len /* || isRepeat */)
		{
			mPos++;
			playInternal();
			broadcastAdvance();
		}
		else
		{
			long songId;

			if ((songId = mImpliedNext.getNextSongWithSideEffects()) >= 0)
			{
				mPlaylist.add(songId);
				mPos++;
				playInternal();
				broadcastAdvance();
			}
			else
			{
				stopInternal();
			}
		}
	}

	private void getContentFixup(MyContentObserver o, ContentState state)
	{
		/* Can't handle this yet. */
		assert state.state != ContentState.NOT_FOUND;

		long songId = o.getSongId();

		if (state.state == ContentState.IN_PROCESS)
		{
			Log.d(TAG, "Downloading songId=" + songId);

			/* There should never be more than one download active at
			 * a time. */
			for (MyContentObserver mo: mDownloads.values())
			{
				Log.d(TAG, "Halting download for songId=" + mo.getSongId());
				
				try {
					mService.stopDownload(mo.getContentId());
				} catch (DeadObjectException e) {}
			}

			mDownloads.clear();

			mIsBuffering = true;
			mSongPlayingHack = songId;
			mDownloads.put(songId, o);
		}
		else
			mSongPlayingHack = -1;		
	}

	private ContentState playInternal()
	{
		assert mService != null;

		Log.d(TAG, "playInternal()");

		if (mPlaylist.isEmpty() == true)
			return null;

		if (mPos < 0)
			mPos = 0;

		long songId = mPlaylist.get(mPos);

		Log.d(TAG, "songId=" + songId);

		if (mIsPaused == true || mIsPlaying == false)
		{
			if (mIsPaused == true)
				mIsPaused = false;

			if (mIsPlaying == false)
				mIsPlaying = true;

			broadcastPlay();
		}

		Log.d(TAG, "Accessing songId=" + songId);

		notifyIfNot();

		ContentState state = null;

		long contentId = getContentId(songId);

		try
		{
			mPending.add(contentId);
			
			MyContentObserver o;
			
			if ((o = mDownloads.get(songId)) != null)
			{
				/* We trust that this song will start playback when the
				 * next buffer update happens since we now have it in
				 * the mPending set. */
				mSongPlayingHack = songId;
				mIsBuffering = true;
			}
			else
			{
				o = new MyContentObserver(contentId, songId);
				state = mService.getContent(contentId, o);
				getContentFixup(o, state);
			}
		}
		catch (DeadObjectException e)
		{
			final String msg = e.toString();

			mHandler.post(new Runnable() {
				public void run()
				{
					Toast.makeText(PlaylistService.this, "BUG: " + msg,
					  Toast.LENGTH_SHORT).show();
				}
			});
		}

		return state;
	}
	
	private void pauseInternal()
	{
		if (mIsPaused == true)
			return;

		if (mIsPlaying == true)
		{
			mPlayer.pause();
			mIsPaused = true;
		}
	}
	
	private void unpauseInternal()
	{
		if (mIsPaused == false)
			return;

		mPlayer.start();
		mIsPaused = false;
	}

	private void stopInternal()
	{
		mPos = -1;
		
		if (mPlayer != null)
			mPlayer.stop();

		if (mIsPlaying == true)
		{
			mIsPlaying = false;
			broadcastStop();
		}

		/* XXX: Eventually this should be removed when streaming works
		 * as expected. */
		mIsBuffering = false;
		mPending.clear();
		mSongPlayingHack = -1; 
	}
	
	private List<Long> getPlaylistWindowInternal(int from, int to)
	{		
		int n = mPlaylist.size();

		if (n == 0)
			return null;

		if (from < 0)
			return null;

		if (to > n)
			to = n;

		if (from >= to)
			return null;

		return mPlaylist.subList(from, to);
	}

	private void checkIfNewLastSong()
	{
		int n = mPlaylist.size();

		if (n == 0)
			return;

		long songId = mPlaylist.get(n - 1);

		if (mImpliedNext.getLastSongId() != songId)
			mImpliedNext.setLastSongId(songId);
	}

	private final IPlaylistService.Stub mBinder = new IPlaylistService.Stub()
	{
		private final Runnable mPreempt = new Runnable()
		{
			public void run()
			{
				try {
					preemptiveDownloadCheck();
				} catch (DeadObjectException e) {}				
			}
		};
		
		public int enqueueNext(long songId)
		{
			int pos;

			if (mPos < 0)
			{
				assert mIsPlaying == false;
				enqueue(songId);
				pos = mPlaylist.size() - 1;
			}
			else
			{
				mPlaylist.add(mPos + 1, songId);
				checkIfNewLastSong();
				pos = mPos + 1;

				mHandler.post(mPreempt);
			}

			return pos;
		}

		public int enqueueNextAndPlay(long songId)
		{
			if (mPos < 0)
			{
				assert mIsPlaying == false;
				enqueue(songId);				
			}
			else
			{
				mPlaylist.add(++mPos, songId);
				checkIfNewLastSong();

				if (mPlayer != null)
					mPlayer.stop();

				playInternal();
				broadcastJump(mPos);
			}

			return mPos;
		}
		
		public int enqueue(long songId)
		{
			mPlaylist.add(songId);
			checkIfNewLastSong();

			int pos = mPlaylist.size() - 1;

			if (mIsPlaying == false)
			{
				mPos = pos;
				playInternal();
			}
			else
			{
				mHandler.post(mPreempt);
			}

			return pos - mPos;
		}

		public int enqueueAndPlay(long songId)
		{
			if (mPos < 0)
			{
				assert mIsPlaying == false;
				enqueue(songId);				
			}
			else
			{
				mPlaylist.add(songId);
				checkIfNewLastSong();

				if (mPlayer != null)
					mPlayer.stop();

				mPos = mPlaylist.size() - 1;
				playInternal();
				broadcastJump(mPos);
			}

			return mPos;			
		}
		
		public int enqueueAlbum(long songId)
		{
			return 0;
		}

		public int enqueueArtist(long songId)
		{
			return 0;
		}

		public int getPosition()
		{
			return mPos;
		}

		public int insert(long songId, int position)
		{
			return 0;
		}

		public int insertAlbum(long albumId, int position)
		{
			return 0;
		}

		public int insertArtist(long artistId, int position)
		{
			return 0;
		}

		public boolean isPaused()
		{
			return mIsPaused;
		}

		public boolean isPlaying()
		{
			return mIsPlaying;
		}

		public boolean isStopped()
		{
			return (mIsPlaying == false);
		}

		public void jump(int pos)
		{
			
		}

		public int next()
		{
			jumpInternal(mPos + 1);
			return mPos;
		}
		
		public void unpause()
		{
			unpauseInternal();
		}

		public void pause()
		{
			pauseInternal();
		}

		public void play()
		{
			playInternal();
		}

		public int previous()
		{
			jumpInternal(mPos - 1);
			return mPos;
		}

		public void remove(long songId)
		{
		}

		public void seek(long pos)
		{
		}

		public void setAdvance(boolean advance)
		{
		}

		public void setRandom(boolean random)
		{
		}

		public void setRepeat(boolean repeat)
		{
		}

		public void shuffle()
		{
		}

		public void skip(long offs)
		{
		}

		public void stop()
		{
			stopInternal();
		}

		public long tell()
		{
			return 0;
		}

		public void tidy()
		{
		}

		public long getSongAt(int position)
		{
			if (position < 0 || position >= mPlaylist.size())
				return -1;

			return mPlaylist.get(position);
		}

		public boolean isBuffering()
		{
			return mIsBuffering;
		}

		public void registerOnBufferListener(IPlaylistBufferListener l)
		{
			mBufferListeners.register(l);
		}

		public void unregisterOnBufferListener(IPlaylistBufferListener l)
		{
			mBufferListeners.unregister(l);
		}

		public void registerOnMoveListener(IPlaylistMoveListener l)
		{
			mMoveListeners.register(l);
		}

		public void unregisterOnMoveListener(IPlaylistMoveListener l)
		{
			mMoveListeners.unregister(l);
		}

		public List getPlaylist()
		{
			return mPlaylist;
		}

		public List getPlaylistWindow(int from, int to)
		{
			/* Note that we are not actually releasing a view of this
			 * list because it is simply being serialized and then discarded.
			 * The usual contract of this method does not apply. */
			return getPlaylistWindowInternal(from, to);
		}

		public int getSongDuration()
		{
			if (mIsPlaying == false)
				return -1;

			return mPlayer.getDuration();
		}

		public int getSongPosition()
		{
			if (mIsPlaying == false)
				return -1;

			return mPlayer.getCurrentPosition();
		}

		public boolean isInPlaylist(long songId)
		{
			return mPlaylist.contains(songId);
		}
	};

	/* XXX: Remove when MediaPlayer supports simultaneous streaming and
	 * caching. */
	private void restartPlaybackHack()
	  throws Exception
	{
		assert mPlayer != null;
		assert mPlayingPathHack != null;
		
		Log.d(TAG, "Engaging streaming hack (THIS IS NECESSARY DUE TO ANDROID BUGS!)");

		int pos = mPlayer.getCurrentPosition();

		mPlayer.reset();

		mPlayer.setDataSource(mPlayingPathHack);
		mPlayer.prepare();

		mPlayer.seekTo(pos);

		mPlayer.start();
	}

	private void notifyIfNot()
	{
		if (mNotificationUp == true)
			return;

//		Intent intent = new Intent(this, Playlist.class);
//
//		mNM.notify(R.string.app_name, new Notification(
//			this,
//			R.drawable.player_notif,
//			null,
//			System.currentTimeMillis(),
//			"This is a placeholder",
//			"Notifications need fixing.",
//			intent,
//			R.drawable.icon,
//			getText(R.string.app_name),
//			intent
//		));
//
//		mNotificationUp = true;
	}
	
	private void broadcastPlay()
	{
		int n = mMoveListeners.beginBroadcast();
		
		while (n-- > 0)
		{
			try { mMoveListeners.getBroadcastItem(n).play(); }
			catch (DeadObjectException e) { }
		}
		
		mMoveListeners.finishBroadcast();
	}
	
	private void broadcastJump(int pos)
	{
		int n = mMoveListeners.beginBroadcast();
		
		while (n-- > 0)
		{
			try { mMoveListeners.getBroadcastItem(n).jump(pos); }
			catch (DeadObjectException e) { }
		}
		
		mMoveListeners.finishBroadcast();		
	}

	private void broadcastAdvance()
	{
		int n = mMoveListeners.beginBroadcast();
		
		while (n-- > 0)
		{
			try { mMoveListeners.getBroadcastItem(n).advance(); }
			catch (DeadObjectException e) { }
		}
		
		mMoveListeners.finishBroadcast();
	}

	private void broadcastPause()
	{
		int n = mMoveListeners.beginBroadcast();

		while (n-- > 0)
		{
			try { mMoveListeners.getBroadcastItem(n).pause(); }
			catch (DeadObjectException e) { }
		}

		mMoveListeners.finishBroadcast();
	}

	private void broadcastStop()
	{
		int n = mMoveListeners.beginBroadcast();

		while (n-- > 0)
		{
			try { mMoveListeners.getBroadcastItem(n).stop(); }
			catch (DeadObjectException e) { }
		}

		mMoveListeners.finishBroadcast();
	}

	private void broadcastStreamStart(long songId)
	{
		int n = mBufferListeners.beginBroadcast();
		
		while (n-- > 0)
		{
			try { mBufferListeners.getBroadcastItem(n).onStreamStart(songId); }
			catch (DeadObjectException e) { }
		}
		
		mBufferListeners.finishBroadcast();
	}

	private void broadcastBufferUpdate(long songId, int pct, int bufPct)
	{
		int n = mBufferListeners.beginBroadcast();

		while (n-- > 0)
		{
			try { mBufferListeners.getBroadcastItem(n).onBufferUpdate(songId, pct, bufPct); }
			catch (DeadObjectException e) { }
		}

		mBufferListeners.finishBroadcast();		
	}
	
	private void startPlayback(long contentId, Uri uri)
	  throws Exception
	{
		Cursor c = getContentResolver().query(uri,
		  new String[] { "_data" }, null, null, null);

		assert c.count() == 1;

		c.first();
		String path = c.getString(0);

		c.close();

		Log.d(TAG, "Starting playback...");

		if (mPlayer != null)
			mPlayer.reset();
		else
		{
			mPlayer = new MediaPlayer();
			mPlayer.setOnCompletionListener(mSongFinished);			
		}

		mPlayingPathHack = path;
		mPlayer.setDataSource(path);
		mPlayer.prepare();

		mPlayer.start();
	}
	
	private MyContentObserver preemptiveDownloadCheckDo(long songId)
	  throws DeadObjectException
	{
		long contentId = getContentId(songId);
		
		ContentState state;

		try {
			state = mService.testContent(contentId);
		} catch (DeadObjectException e) { return null; }

		if (state.state == ContentState.IN_CACHE ||
		    state.state == ContentState.NOT_FOUND)
			return null;
		
		Log.d(TAG, "...we've got work to do, preempting songId=" + songId);

		MyContentObserver o = new MyContentObserver(contentId, songId);
		ContentState newstate = mService.getContent(contentId, o);

		if (state.state == ContentState.IN_PROCESS)
			mDownloads.put(songId, o);

		return o;		
	}
	
	private MyContentObserver preemptiveDownloadCheck()
	  throws DeadObjectException
	{
		Log.d(TAG, "Starting preemptive download check...");
		
		if (mIsPlaying == false || mIsPaused == true)
			return null;

		if (mDownloads.isEmpty() == false)
		{
			Log.d(TAG, "...download already active; give up.");
			return null;
		}

		int size = mPlaylist.size();

		if (mPos + 1 < size)
		{
			assert mPos >= 0;

			/* Preempt the next 5 songs. */
			List<Long> nextUp = getPlaylistWindowInternal(mPos + 1, mPos + 6);

			for (Long songId: nextUp)
			{
				MyContentObserver o;

				if ((o = preemptiveDownloadCheckDo(songId)) != null)
					return o;
			}
		}
		else
		{
			MyContentObserver o;
			long songId;

			if ((songId = mImpliedNext.peekNextSong()) >= 0)
			{
				if ((o = preemptiveDownloadCheckDo(songId)) != null)
				{
					Log.d(TAG, "Woooo, caching implied next song, songId=" + songId);
					return o;
				}
				else
				{
					Log.d(TAG, "Implied next track is already queued, great!");
				}
			}
		}

		Log.d(TAG, "Everything in the playlist is cached...giving up.");

		return null;
	}

	private class MyContentObserver extends IContentObserver.Stub
	{
		private long mSongId;
		private long mContentId;

		public MyContentObserver(long contentId, long songId)
		{
			mContentId = contentId;
			mSongId = songId;
		}

		public long getContentId() { return mContentId; }
		public long getSongId() { return mSongId; }

		public void error(final long contentId, final String message)
		{
			/* OMG, this is terrible! FIX ME! */
			if (message.contains("InterruptedException") == true)
				return;

			mHandler.post(new Runnable() {
				public void run()
				{
					Toast.makeText(PlaylistService.this, "BUG: " + message,
					  Toast.LENGTH_SHORT).show();

					if (mPending.remove(contentId) == true)
					{
						/* If the media player never got a chance to
						 * process this item, simply move on now.  Otherwise,
						 * let the song run up to the length that has been
						 * downloaded and get interrupted normally. */
						if (mPlayer == null || mPlayer.isPlaying() == false)
							advanceInternal();
					}
				}
			});
		}

		private boolean tryPlay(final long contentId, final Uri uri)
		{
			/* TODO: This should be removed.  This is a stupid way to
			 * manage this system. */
			if (mPending.remove(contentId) == false)
				return false;

			Log.d(TAG, "tryPlay(" + contentId + ", " + uri + "), success!");

			mHandler.post(new Runnable() {
				public void run()
				{
					if (mPlaylist.get(mPos) != mSongId || mIsPaused == true)
					{
						Log.d(TAG, "NO, DON'T PLAY!");
						return;
					}

					try {
						startPlayback(contentId, uri);
					} catch (Exception e) {
						Toast.makeText(PlaylistService.this,
						  "ANDROID BUG: Try again, *sigh*",
						  Toast.LENGTH_SHORT).show();
						
						Log.d(TAG, "Eeep! MediaPlayer is goofy...", e);
						stopInternal();
					} finally {
						mIsBuffering = false;
					}
				}
			});

			return true;
		}

		public void finished(long contentId, Uri uri, ContentState state)
		{
			Log.d(TAG, "Download done!");
			tryPlay(contentId, uri);

			mHandler.post(new Runnable() {
				public void run()
				{
					mDownloads.remove(mSongId);
					
					try {
						preemptiveDownloadCheck();
					} catch (DeadObjectException e) {}

					if (mSongId != mSongPlayingHack || mIsPaused == true)
						return;

					try {
						restartPlaybackHack();
					} catch (Exception e) {
						Toast.makeText(PlaylistService.this,
						  "ANDROID BUG: Try again, *sigh*",
						  Toast.LENGTH_SHORT).show();

						Log.d(TAG, "Eeek!", e);
						stopInternal();
					}
				}
			});
		}

		public long updateInterval()
		{
			return 500;
		}

		public void updateProgress(long contentId, Uri uri, ContentState state)
		{
			float pct = state.getPercentage();

			/* TODO: Buffer fill percentage should be much, much more
			 * sophisticated than this.  Unfortunately, the semantics would
			 * need to change some and I'm not prepared to make that change
			 * now. */
			int bufPct = (int)(pct * 1000);
			
			if (bufPct > 100)
				bufPct = 100;

			Log.d(TAG, "Fire IPlaylistBufferListeners, pct=" + pct + ", bufPct=" + bufPct);
			
			broadcastBufferUpdate(mSongId, (int)(pct * 100), bufPct);

			if (pct >= 0.10)
			{
				if (tryPlay(contentId, uri) == true)
					broadcastStreamStart(mSongId);
			}
		}
	};

	/* XXX: Stupid hack that will go away. */
	private static class NextSongGenerator
	{
		private ContentResolver mContent;
		
		private Cursor mNextAlbum = null;
		private long mLastSongId = -1;

		public static final String[] PROJECTION = {
			"_id", 
		};

		public NextSongGenerator(ContentResolver cr)
		{
			mContent = cr;
		}

		public synchronized long getLastSongId()
		{
			return mLastSongId;
		}

		public synchronized void setLastSongId(long songId)
		{
			if (mNextAlbum != null)
			{
				mNextAlbum.close();
				mNextAlbum = null;
			}

			mLastSongId = songId;
			
			Log.d(TAG, "setLastSongId: songId=" + songId);

			if (songId == -1)
				return;

			Cursor c = mContent.query(ContentUris.withAppendedId(Five.Music.Songs.CONTENT_URI, songId),
			  new String[] { Five.Music.Songs._ID, Five.Music.Songs.ALBUM_ID },
			  null, null, null);

			/* What? */
			if (c == null || c.count() == 0)
			{
				Log.d(TAG, "Uhh, huh?");
				
				c.close();
				return;
			}

			c.first();
			long albumId = c.getLong(1);
			c.close();
			
			Log.d(TAG, "hooked onto albumId=" + albumId);

			Uri songs = Five.Music.Albums.CONTENT_URI.buildUpon()
			  .appendPath(String.valueOf(albumId))
			  .appendPath("songs")
			  .build();

			mNextAlbum = mContent.query(songs, PROJECTION,
			  null, null, "track_num ASC, title ASC");

			positionCursor();
		}
		
		private void positionCursor()
		{
			if (mNextAlbum == null)
				return;

			Log.d(TAG, "Attempting to position cursor...");

			/* Scan the cursor to position it one after where the current
			 * last playing song is. */
			while (mNextAlbum.next() == true)
			{
				long songId = mNextAlbum.getLong(0);

				if (songId == mLastSongId)
				{
					mNextAlbum.next();
					Log.d(TAG, "Positioned at " + mNextAlbum.position());
					return;
				}
			}
			
			Log.d(TAG, "...gah, failed!");
		}
		
		public synchronized long peekNextSong()
		{
			Log.d(TAG, "peekNextSong()");
			
			if (mNextAlbum == null)
				return -1;
			
			if (mNextAlbum.isBeforeFirst() == true ||
			    mNextAlbum.isAfterLast() == true)
				return -1;
			
			long songId = mNextAlbum.getLong(0);
			
			Log.d(TAG, "...got songId=" + songId);
			
			return songId;
		}

		public synchronized long getNextSongWithSideEffects()
		{
			Log.d(TAG, "peekNextSongWithSideEffects()");
			
			long songId = peekNextSong();
			
			mLastSongId = songId;

			if (songId >= 0)
				mNextAlbum.next();

			return songId;
		}
		
		public void release()
		{
			if (mNextAlbum != null)
			{
				mNextAlbum.close();
				mNextAlbum = null;
			}
		}
	}
}
